/*
Copyright (c) 2007, EffectiveUI, Inc.

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the EffectiveUI nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@ignore
*/
package com.effectiveui.frameworks.commando {
	
	import com.effectiveui.frameworks.commando.error.*;
	import com.effectiveui.frameworks.commando.event.CommandoEvent;
	
	import flash.utils.Dictionary;
	
	import mx.utils.StringUtil;
	
	/**
	 * <p>
	 * CommandoInterpereter is responsible for interpereting
	 * a command line string and possibly executing a command based on that string.
	 * </p>
	 * 
	 * <p>
	 * CommandoInterpereter holds the definitions of all the commands 
	 * known to the application. It is a singleton. CommandoInterpereter
	 * also exposes utilty methods for working with command line strings.
	 * </p>
	 * 
	 */	
	public class CommandoInterpereter {
		
		private static var instance:CommandoInterpereter;
		
		private var verbs:Dictionary = new Dictionary();
		
		/**
		 * CommandoInterpereter is a singleton.
		 * 
		 * @return the singleton instance
		 * 
		 */		
		public static function getInstance():CommandoInterpereter {
			if (instance == null) instance = new CommandoInterpereter(new Initializer());
			return instance;
		}
		
		/**
		 * Should never be called by anything but CommandoInterpereter.
		 * 
		 * @param init 
		 * 
		 */		
		public function CommandoInterpereter(init:Initializer) {
			if (init == null) throw new Error("Interpereter can only be accessed through Interpereter.getInstance()");
		}
		
		/**
		 * Given a command line string, execute corresponding command.
		 * Default behaviour is to throw an error if the options are invalid, i.e.
		 * required options are missing, required arguments are missing, etc. This 
		 * can be configured to return false on invalid instead.
		 * 
		 * @param commandString string to parse and execute.
		 * @param errorOnInvalidOptions should execute throw an error if given invalid options?
		 * @return Boolean validity of options if set to not error on invalid options.
		 * 
		 */		
		public function execute(commandString:String, errorOnInvalidOptions:Boolean=true, failOnUnknownOption:Boolean=true):Boolean {
			var options:Options;
			var parsedOptions:Dictionary;
			var optionsValid:Boolean;
			var verb:String = parseVerb(commandString);
			var remainder:String = subtractVerb(verb, commandString);
			if (!isRegisteredVerb(verb)) throw new NoSuchCommandError(verb);
			
			if (remainder != '') {
				parsedOptions = parseOpts(remainder);
			}
			
			if (verbs[verb] is Options) { // we're expecting options
				options = Options(verbs[verb]).clone(); //important to clone so we have a fresh copy to add values to.
				optionsValid = options.validate(parsedOptions, failOnUnknownOption);
				if (!optionsValid) {
					if (errorOnInvalidOptions) throw new InvalidOptionsError(verb, parsedOptions, options);
					return false;
				}
				options.addValues(parsedOptions);
			} else {
				if (parsedOptions != null) {
					if (errorOnInvalidOptions) throw new InvalidOptionsError(verb, parsedOptions, null);
					return false;
				}
			}
			
			var event:CommandoEvent = new CommandoEvent(verb, options);
			event.dispatch();
			return true;
		}
		
		/**
		 * Is this verb registered to a command?
		 * 
		 * @param verb is a command name
		 * @return Boolean registered.
		 * 
		 */		
		public function isRegisteredVerb(verb:String):Boolean {
			return (verbs[verb] != null);
		}
		
		/**
		 * Registers verb with optional options.
		 * 
		 * @param verb command name
		 * @param options optional options.
		 * 
		 */		
		public function registerVerb(verb:String, options:Options=null):void {
			if (verbs[verb] != null) throw new CommandoError("Verb: " + verb + " is already registered in Interpereter");
			if (options == null) {
				verbs[verb] = false;
			} else {
				verbs[verb] = options;
			}
		}
		
		/**
		 * Does this verb have options?
		 * 
		 * @param verb command name
		 * @return Boolean has options
		 * 
		 */		
		public function verbTakesOptions(verb:String):Boolean {
			return (isRegisteredVerb(verb) && verbs[verb] is Options);
		}
		
		/**
		 * Unregister command name
		 * 
		 * @param verb command name
		 * 
		 */		
		public function unregisterVerb(verb:String):void {
			verbs[verb] = null;
			delete verbs[verb];
		}
		
		/**
		 * <b>UNIMPLEMENTED</b> Given a verb fragment return
		 * an array of matching verbs. This allows an application
		 * to implement 'auto-complete' functionality.
		 * 
		 * @param verbFragment string
		 * @return Array of matching verbs.
		 * 
		 */		
		public function autoComplete(verbFragment:String):Array {
			return null;
		}
		
		/**
		 * Given a command string, return the verb. Command lines
		 * are space delimited with the verb as the first string.
		 * 
		 * @param commandString command line.
		 * @return verb component of command line.
		 * 
		 */		
		public function parseVerb(commandString:String):String {
			return commandString.split(' ')[0];
		}
		
		/**
		 * Given a verb, remove verb component from command line
		 * and return the rest.
		 * 
		 * @param verb command name.
		 * @param commandLine command line.
		 * @return command line after verb.
		 * 
		 */		
		public function subtractVerb(verb:String, commandLine:String):String {
			var newCommandLine:String = commandLine.substring(verb.length, commandLine.length);
			
			return newCommandLine;
		}
		
		/**
		 * Given a command string (minus the verb) parse the options
		 * and arguments into a Dictionary.
		 * 
		 * @param commandString command line (minus the verb)
		 * @return Dictionary of parsed options
		 * 
		 */		
		public function parseOpts(commandString:String):Dictionary {
			var option:String;
			var token:String;
			var hasNext:Boolean;
			var nextToken:String;
			var optionStartIndex:int;
			var parsedOptions:Dictionary	= new Dictionary();
			var args:String					= StringUtil.trim(commandString);
			
			var tokens:Array = parseTokens(args);
			
			for (var i:int=0; i<tokens.length; i++) {
				token = String(tokens[i]);
				hasNext = (i < (tokens.length-1));
				optionStartIndex = 1;
				if (token.indexOf('-') == 0) {//an option! an option!
					if (token.charAt(1) == '-') optionStartIndex = 2;
					option = token.substring(optionStartIndex, token.length);
					if (hasNext) {
						nextToken = tokens[i+1];
						if (nextToken.indexOf('-') == 0) { // another option. current one must be a no-arg option
							parsedOptions[option] = null;
						} else {
							parsedOptions[option] = nextToken;
						}
					} else { // end of the options, this one must be a no-arg option
						parsedOptions[option] = null;
					}
				}
			}
			
			return parsedOptions;
		}
		
		/**
		 * 
		 * This method is a simple 'stream parser' to break a 
		 * command line into a list of parsable elements. The Rules:
		 * Options begin with - or -- and are 
		 * of arbitrary length. Spaces delimit options with values.
		 * If a value contains spaces it should be quoted. Either 
		 * single or double quotes may be used. If there are single
		 * quotes in the value, use double quotes, if double, use 
		 * single. If your value contains both, for now the command
		 * cannot be parsed.
		 * 
		 * @param commandLine
		 * @return array of String containing the tokens parsed
		 * from a command line.
		 * 
		 */		
		public function parseTokens(commandLine:String):Array {
			var char:String;
			var previousChar:String = '';
			var tokens:Array = [];
			var token:String = '';
			var parsingOpt:Boolean;
			var parsingValue:Boolean;
			var parsingQuotedValue:Boolean;
			var charIsSpace:Boolean;
			var beginningQuote:String;
			
			// this is a very brute force, but safe?? way to do it. refactor for elegance.
			for (var i:int=0; i < commandLine.length; i++) {
				char = commandLine.charAt(i);
				charIsSpace = (char == ' ');
				if (parsingValue) {
					if (parsingQuotedValue) {
						if (char == beginningQuote) {//end of a quoted value
							parsingQuotedValue = parsingValue = false;
							tokens.push(token);
							token = ''
						} else {
							token = token.concat(char);
						}
					} else {
						if (charIsSpace) {//end of a value
							parsingValue = false;
							tokens.push(token);
							token = ''
						} else {
							token = token.concat(char);
						}
					}
				} else if (parsingOpt) {
					if (charIsSpace) {//end of an opt
						parsingOpt = false;
						tokens.push(token);
						token = ''
					} else {
						token = token.concat(char);
					}
				} else if (char == '-') {
					parsingOpt = true;
					token = char //keep one '-' to denote an option
				} else if (!charIsSpace) {
					parsingValue = true;
					if (char == '"' || char == "'") {
						parsingQuotedValue = true;
						beginningQuote = char;
					} else {
						token = char;
					}
				}
				if (i == (commandLine.length-1) && token != '') {//end of commandline, flush if there's something in there.
					tokens.push(token);
				}
				previousChar = char;
			}
			return tokens;
		}
		
	}
}

class Initializer {}